﻿// (c) Norbert Huffschmid
// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media.Animation;
using System.Windows.Media;
using System.Collections.Specialized;
using System.ComponentModel;
using SpinDotters.Core.InputControl;

namespace SpinDotters.Core
{
    /// <summary>
    /// Base control for spinning controls
    /// </summary>
    [TemplatePart(Name = "PART_LayoutRoot",      Type = typeof(Grid))]
    [TemplatePart(Name = "PART_FlashStoryboard", Type = typeof(Storyboard))]
    [TemplatePart(Name = "PART_ItemsPresenter", Type = typeof(ItemsPresenter))]
    [TemplateVisualState(Name = "Normal",           GroupName = "CommonStates")]
    [TemplateVisualState(Name = "MouseOver",        GroupName = "CommonStates")]
    [TemplateVisualState(Name = "ReadOnly",         GroupName = "CommonStates")]
    [TemplateVisualState(Name = "Disabled",         GroupName = "CommonStates")]
    [TemplateVisualState(Name = "Focused",          GroupName = "FocusStates")]
    [TemplateVisualState(Name = "Unfocused",        GroupName = "FocusStates")]
    [TemplateVisualState(Name = "Reflective",       GroupName = "ReflectionStates")]
    [TemplateVisualState(Name = "Unreflective",     GroupName = "ReflectionStates")]
    [TemplateVisualState(Name = "Vertical",         GroupName = "OrientationStates")]
    [TemplateVisualState(Name = "Horizontal",       GroupName = "OrientationStates")]
    public abstract class UpDownBase : ItemsControl
    {
        internal const int ANIMATION_TIME_DEFAULT = 500; // msec
        internal const bool REFLECTIVE_DEFAULT = false;
        internal const Orientation ORIENTATION_DEFAULT = Orientation.Vertical;

        private const int SPINNING_ITEMS_PER_GESTURE = 4;

        /// <summary>
        /// Event fired when spin orientation has changed.
        /// </summary>
        [Category("SpinDotters")]
        public event EventHandler OrientationChanged;


        protected event RoutedEventHandler SelectedItemChanged;

        protected ItemsPresenter itemsPresenter;
        protected FrameworkElement layoutRoot;

        private const bool READONLY_DEFAULT = false;

        protected SpinPanel spinPanel;
        private Storyboard flashStoryboard;

        /// <summary>
        /// SelectedIndex dependency property.
        /// </summary>
        protected static readonly DependencyProperty SelectedIndexProperty =
            DependencyProperty.Register(
                "SelectedIndex",
                typeof(int),
                typeof(UpDownBase),
                new PropertyMetadata(-1, new PropertyChangedCallback(OnSelectedIndexChanged))
            );

        /// <summary>
        /// Selected index
        /// </summary>
        protected int SelectedIndex
        {
            get
            {
                int selectedIndex = (int)GetValue(SelectedIndexProperty);

                if ((this.Items.Count == 0) || (selectedIndex == -1))
                    return -1;
                else
                    return Normalize(selectedIndex);
            }
            
            set
            {
                SetValue(SelectedIndexProperty, Normalize(value));
            }
        }

        /// <summary>
        /// AnimationTime dependency property.
        /// </summary>
        public static readonly DependencyProperty AnimationTimeProperty =
            DependencyProperty.Register(
                "AnimationTime",
                typeof(int),
                typeof(UpDownBase),
                null);

        /// <summary>
        /// Animation time in milliseconds.
        /// </summary>
        [Category("SpinDotters")]
        public int AnimationTime
        {
            get { return (int)GetValue(AnimationTimeProperty); }
            set { SetValue(AnimationTimeProperty, value); }
        }

        /// <summary>
        /// Reflective dependency property.
        /// </summary>
        public static readonly DependencyProperty ReadOnlyProperty =
            DependencyProperty.Register(
                "ReadOnly",
                typeof(bool),
                typeof(UpDownBase),
                new PropertyMetadata(READONLY_DEFAULT, OnReadOnlyChanged));

        /// <summary>
        /// Indicates whether or not the control is read-only.
        /// </summary>
        [Category("SpinDotters")]
        public bool ReadOnly
        {
            get { return (bool)GetValue(ReadOnlyProperty); }
            set { SetValue(ReadOnlyProperty, value); }
        }

        /// <summary>
        /// Reflective dependency property.
        /// </summary>
        public static readonly DependencyProperty ReflectiveProperty =
            DependencyProperty.Register(
                "Reflective",
                typeof(bool),
                typeof(UpDownBase),
                new PropertyMetadata(REFLECTIVE_DEFAULT, OnReflectiveChanged));

        /// <summary>
        /// Indicates whether or not the control shows an reflective effect.
        /// </summary>
        [Category("SpinDotters")]
        public bool Reflective
        {
            get { return (bool)GetValue(ReflectiveProperty); }
            set { SetValue(ReflectiveProperty, value); }
        }

        /// <summary>
        /// Orientation dependency property.
        /// </summary>
        public static readonly DependencyProperty OrientationProperty =
            DependencyProperty.Register(
                "Orientation",
                typeof(Orientation),
                typeof(UpDownBase),
                new PropertyMetadata(UpDownBase.ORIENTATION_DEFAULT,
                    new PropertyChangedCallback(OnOrientationChanged)));

        /// <summary>
        /// Gets or sets the spin orientation.
        /// </summary>
        public Orientation Orientation
        {
            get { return (Orientation)GetValue(OrientationProperty); }
            set { SetValue(OrientationProperty, value); }
        }

        /// <summary>
        /// Spin angle property.
        /// </summary>
        public static readonly DependencyProperty SpinAngleProperty =
            DependencyProperty.Register(
                "SpinAngle",
                typeof(double),
                typeof(UpDownBase),
                null);

        /// <summary>
        /// Gets or sets the spin angle.
        /// </summary>
        public double SpinAngle
        {
            get { return (double)GetValue(SpinAngleProperty); }
            set { SetValue(SpinAngleProperty, value); }
        }

        /// <summary>
        /// Dragged dependency property.
        /// </summary>
        public static readonly DependencyProperty DraggedProperty =
            DependencyProperty.Register(
                "Dragged",
                typeof(bool),
                typeof(UpDownBase),
                new PropertyMetadata(new PropertyChangedCallback(OnDraggedChanged)));

        /// <summary>
        /// Gets or sets the dragged condition.
        /// </summary>
        public bool Dragged
        {
            get { return (bool)GetValue(DraggedProperty); }
            set { SetValue(DraggedProperty, value); }
        }


        /// <summary>
        /// Default constructor
        /// </summary>
        public UpDownBase()
        {
            // set default style
            this.DefaultStyleKey = typeof(UpDownBase);

            // set default values for dependency properties
            this.AnimationTime = ANIMATION_TIME_DEFAULT;

            // register for events
            this.MouseEnter += new MouseEventHandler(UpDownBase_MouseEnter);
            this.MouseLeave += new MouseEventHandler(UpDownBase_MouseLeave);
            this.MouseLeftButtonDown += new MouseButtonEventHandler(UpDownBase_MouseLeftButtonDown);
            this.GotFocus += new RoutedEventHandler(UpDownBase_GotFocus);
            this.LostFocus += new RoutedEventHandler(UpDownBase_LostFocus);
            this.IsEnabledChanged += new DependencyPropertyChangedEventHandler(UpDownBase_IsEnabledChanged);
            this.SizeChanged += new SizeChangedEventHandler(UpDownBase_SizeChanged);
            ((INotifyCollectionChanged)this.Items).CollectionChanged += new NotifyCollectionChangedEventHandler(UpDownBase_CollectionChanged);
        }

        /// <summary>
        /// Spin upwards
        /// </summary>
        /// <param name="steps">Number of spin steps</param>
        public void SpinUp(int steps)
        {
            this.SelectedIndex += steps;
        }

        /// <summary>
        /// Spin downwards
        /// </summary>
        /// <param name="steps">Number of spin steps</param>
        public void SpinDown(int steps)
        {
            SpinUp(-steps);
        }

        /// <summary>
        /// Show flash animation, e.g. for error indication
        /// </summary>
        /// <param name="flashCounter">Number of flashes</param>
        /// <param name="flashInterval">Interval between flash states in milliseconds</param>
        public void Flash(int flashCounter, int flashInterval)
        {
            if (this.flashStoryboard != null)
            {
                // set repeat behavior
                this.flashStoryboard.RepeatBehavior = new RepeatBehavior(flashCounter);

                // override duration of each child animation
                foreach (Timeline animation in this.flashStoryboard.Children)
                {
                    animation.Duration = new Duration(new TimeSpan(0, 0, 0, 0, flashInterval));
                }

                this.flashStoryboard.Begin();
            }
        }

        /// <summary>
        /// Bind to a dragging control, e.g. JogDial
        /// </summary>
        /// <param name="dragControl">Drag control instance</param>
        public void BindToDragControl(IDragControl dragControl)
        {
            double localSpinAngle = this.SpinAngle;

            Binding spinAngleBinding = new Binding("SpinAngleProperty");
            spinAngleBinding.Source = dragControl;
            spinAngleBinding.Path = new PropertyPath("SpinAngle");
            spinAngleBinding.Mode = BindingMode.TwoWay;
            this.SetBinding(SpinAngleProperty, spinAngleBinding);

            // sync drag control with current local value
            this.SpinAngle = localSpinAngle;

            Binding dragBinding = new Binding("DraggedProperty");
            dragBinding.Source = dragControl;
            dragBinding.Path = new PropertyPath("Dragging");
            dragBinding.Mode = BindingMode.OneWay;
            this.SetBinding(DraggedProperty, dragBinding);
        }

        /// <summary>
        /// Unbind from a dragging control, e.g. JogDial
        /// </summary>
        public void UnbindDragControl()
        {
            double localSpinAngle = this.SpinAngle;

            // clear spinangle binding (and local value)
            this.ClearValue(SpinAngleProperty);

            // retain local value
            this.SpinAngle = localSpinAngle; 

            // clear drag binding
            this.ClearValue(DraggedProperty);
        }

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            // determine storyboards and register event handlers
            this.layoutRoot = this.GetTemplateChild("PART_LayoutRoot") as FrameworkElement;
            if (layoutRoot != null)
            {
                this.flashStoryboard = layoutRoot.Resources["PART_FlashStoryboard"] as Storyboard;
            }

            if (this.Reflective)
                VisualStateManager.GoToState(this, "Reflective", false);
            else
                VisualStateManager.GoToState(this, "Unreflective", false);

            if (this.Orientation == Orientation.Vertical)
                VisualStateManager.GoToState(this, "Vertical", false);
            else
                VisualStateManager.GoToState(this, "Horizontal", false);

            this.itemsPresenter = this.GetTemplateChild("PART_ItemsPresenter") as ItemsPresenter;
        }


        protected virtual void OnOrientationChanged()
        {
            if (OrientationChanged != null)
                OrientationChanged(this, EventArgs.Empty);
        }

        protected override void PrepareContainerForItemOverride(DependencyObject element, Object item)
        {
            base.PrepareContainerForItemOverride(element, item);

            // trick to access ItemsPanel (inspired by Silverlight Toolkit)
            DependencyObject container = this.ItemContainerGenerator.ContainerFromIndex(0);
            if ((container != null) && (this.spinPanel == null))
            {
                this.spinPanel = VisualTreeHelper.GetParent(container) as SpinPanel;

                if (this.spinPanel != null)
                {
                    // doing these bindings in XAML will cause BindingExpression path errors within widgets!

                    Binding spinAngleBinding = new Binding("SpinAngleProperty");
                    spinAngleBinding.Source = this;
                    spinAngleBinding.Path = new PropertyPath("SpinAngle");
                    spinAngleBinding.Mode = BindingMode.TwoWay;
                    this.spinPanel.SetBinding(SpinPanel.SpinAngleProperty, spinAngleBinding);

                    Binding orientationBinding = new Binding("OrientationProperty");
                    orientationBinding.Source = this;
                    orientationBinding.Path = new PropertyPath("Orientation");
                    this.spinPanel.SetBinding(SpinPanel.OrientationProperty, orientationBinding);


                    this.spinPanel.DraggingChanged += new EventHandler(spinPanel_DraggingChanged);
                }
            }
        }

        protected override bool IsItemItsOwnContainerOverride(object item)
        {
            return (item is ContentPresenter);
        }

        protected override DependencyObject GetContainerForItemOverride()
        {
            ContentPresenter contentPresenter = new ContentPresenter();

            Binding horizontalBinding = new Binding("HorizontalAlignmentProperty");
            horizontalBinding.Source = this;
            horizontalBinding.Path = new PropertyPath("HorizontalContentAlignment");
            contentPresenter.SetBinding(HorizontalAlignmentProperty, horizontalBinding);

            Binding verticalBinding = new Binding("VerticalAlignmentProperty");
            verticalBinding.Source = this;
            verticalBinding.Path = new PropertyPath("VerticalContentAlignment");
            contentPresenter.SetBinding(VerticalAlignmentProperty, verticalBinding);

            return contentPresenter;
        }

        protected void SetCurrentDependencyPropertyValue(DependencyProperty dp, object value)
        {
#if WPF
            SetCurrentValue(dp, value);
#else
            // Silverlight workaround for missing SetCurrentValue api
            if (GetBindingExpression(dp) == null)
                SetValue(dp, value);
#endif
        }


        /// <summary>
        /// Determine index from key
        /// </summary>
        /// <param name="key">Key parameter as delivered in KeyEventArgs</param>
        /// <returns>Index of matching item or -1 in case of no matching item</returns>
        protected abstract int DetermineIndexFromKey(Key key);

        private int Normalize(int index)
        {
            int numberOfItems = this.Items.Count;

            if (numberOfItems == 0)
                return -1;

            while (index >= numberOfItems)
                index -= numberOfItems;

            while (index < 0)
                index += numberOfItems;

            return index;
        }

        private void spinPanel_DraggingChanged(object sender, EventArgs e)
        {
            if (!this.spinPanel.Dragging)
            {
                this.SelectedIndex = this.spinPanel.CurrentIndex;

                // snap to nearest matching index value
                this.spinPanel.SpinToChild(this.spinPanel.CurrentIndex, this.AnimationTime);
            }
        }

        private void UpDownBase_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            // this is just a workaround for setting the initial spin angle after initialisation
            // PrepareContainerForItemOverride is too early because children are not constructed yet!!!
            if ((this.Items.Count > 0) && (this.spinPanel != null))
                this.spinPanel.SpinToChild(this.SelectedIndex, this.AnimationTime);
        }

        private void UpDownBase_KeyDown(object sender, KeyEventArgs e)
        {
            if (!this.ReadOnly)
            {
                if (e.Key == Key.Up)
                    this.spinPanel.SpinSteps(1, this.AnimationTime);
                else if (e.Key == Key.Down)
                    this.spinPanel.SpinSteps(-1, this.AnimationTime);
                else
                {
                    int newIndex = DetermineIndexFromKey(e.Key);

                    if (newIndex != -1)
                        this.SelectedIndex = newIndex;
                }
            }
        }

        private void UpDownBase_MouseEnter(object sender, MouseEventArgs e)
        {
            if (!this.ReadOnly)
                VisualStateManager.GoToState(this, "MouseOver", true);
        }

        private void UpDownBase_MouseLeave(object sender, MouseEventArgs e)
        {
            if (!this.ReadOnly)
                VisualStateManager.GoToState(this, "Normal", true);
        }

        private void UpDownBase_GotFocus(object sender, RoutedEventArgs e)
        {
            this.KeyDown += new KeyEventHandler(UpDownBase_KeyDown);
#if WPF
            Application.Current.MainWindow.MouseWheel += new MouseWheelEventHandler(RootVisual_MouseWheel);
#else
            Application.Current.RootVisual.MouseWheel += new MouseWheelEventHandler(RootVisual_MouseWheel);
#endif
            VisualStateManager.GoToState(this, "Focused", true);
        }

        private void UpDownBase_LostFocus(object sender, RoutedEventArgs e)
        {
            this.KeyDown -= new KeyEventHandler(UpDownBase_KeyDown);
#if WPF
            Application.Current.MainWindow.MouseWheel -= new MouseWheelEventHandler(RootVisual_MouseWheel);
#else
            Application.Current.RootVisual.MouseWheel -= new MouseWheelEventHandler(RootVisual_MouseWheel);
#endif
            VisualStateManager.GoToState(this, "Unfocused", true);
        }

        private void UpDownBase_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            if (!this.ReadOnly)
            {
                this.Focus();
            }
        }

        private void UpDownBase_IsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
        {
            if (!this.IsEnabled)
                VisualStateManager.GoToState(this, "Disabled", true);
            else if (this.ReadOnly)
                VisualStateManager.GoToState(this, "ReadOnly", true);
            else
                VisualStateManager.GoToState(this, "Normal", true);
        }

        private void UpDownBase_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (this.Items.Count == 0)
            {
                SetCurrentDependencyPropertyValue(UpDownBase.SelectedIndexProperty, -1);
            }
            else
            {
                if (this.SelectedIndex == -1)
                {
                    // no item selected yet ==> select first item
                    SetCurrentDependencyPropertyValue(UpDownBase.SelectedIndexProperty, 0);
                }
                else
                {
                    // determine new index of selected item
                    int oldSelectedIndex = (int)GetValue(SelectedIndexProperty);
                    if (e.Action == NotifyCollectionChangedAction.Add)
                    {
                        if (e.NewStartingIndex <= oldSelectedIndex)
                            SetCurrentDependencyPropertyValue(UpDownBase.SelectedIndexProperty, oldSelectedIndex + e.NewItems.Count);
                    }
                    else if (e.Action == NotifyCollectionChangedAction.Remove)
                    {
                        if (e.OldStartingIndex <= oldSelectedIndex)
                            SetCurrentDependencyPropertyValue(UpDownBase.SelectedIndexProperty, Normalize(oldSelectedIndex - e.OldItems.Count));
                    }

                    if (this.spinPanel != null)
                    {
                        // spin to proper cylinder angle
                        this.spinPanel.SpinToChild(this.SelectedIndex, this.AnimationTime);
                    }
                }
            }
        }

        private void RootVisual_MouseWheel(object sender, MouseWheelEventArgs e)
        {
            HandleWheelInput(e.Delta);

            e.Handled = true;
        }

        private void HandleWheelInput(int delta)
        {
            if (!this.ReadOnly)
            {
                int steps;

                if (delta > 0)
                    steps = 1;
                else
                    steps = -1;

                this.SelectedIndex += steps;
            }
        }

        private static void OnSelectedIndexChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            UpDownBase sudb = o as UpDownBase;

            if (sudb != null)
            {
                if ((sudb.spinPanel != null) && (sudb.Items.Count > 0))
                {
                    sudb.spinPanel.SpinToChild((int)e.NewValue, sudb.AnimationTime);
                }

                if (sudb.SelectedItemChanged != null)
                    sudb.SelectedItemChanged(sudb, new RoutedEventArgs());
            }
        }

        private static void OnReflectiveChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            UpDownBase sudb = (UpDownBase)o;

            if ((bool)e.NewValue == true)
                VisualStateManager.GoToState(sudb, "Reflective", true);
            else
                VisualStateManager.GoToState(sudb, "Unreflective", true);
        }

        private static void OnReadOnlyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            UpDownBase sudb = (UpDownBase)o;

            // update visual states
            if (!sudb.IsEnabled)
                VisualStateManager.GoToState(sudb, "Disabled", true);
            else if ((bool)e.NewValue == true)
                VisualStateManager.GoToState(sudb, "ReadOnly", true);
            else
                VisualStateManager.GoToState(sudb, "Normal", true);

            // allow/inhibit drag control
            if ((bool)e.NewValue == true)
                sudb.IsHitTestVisible = false;
            else
                sudb.IsHitTestVisible = true;
        }

        private static void OnOrientationChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            UpDownBase sudb = (UpDownBase)o;

            if (sudb != null)
            {
                if ((Orientation)e.NewValue == Orientation.Vertical)
                    VisualStateManager.GoToState(sudb, "Vertical", true);
                else
                    VisualStateManager.GoToState(sudb, "Horizontal", true);

                sudb.OnOrientationChanged();
            }
        }

        private static void OnDraggedChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            UpDownBase sudb = (UpDownBase)o;

            if ((sudb != null) && (sudb.spinPanel != null))
            {
                if (sudb.Dragged)
                {
                    // respond to drag movements immediately
                    sudb.spinPanel.StopAnimation();
                }
                else
                {
                    sudb.SelectedIndex = sudb.spinPanel.CurrentIndex;

                    // snap to nearest matching index value
                    sudb.spinPanel.SpinToChild(sudb.spinPanel.CurrentIndex, sudb.AnimationTime);
                }
            }
        }
    }
}
